package blog

import (
	"appengine"
	"appengine/datastore"
	"appengine/user"
	"crypto/md5"
	"encoding/xml"
	"errors"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"time"
)

type Article struct {
	Title   string
	Time    time.Time
	Updated time.Time
	Content []byte
	Cates   []string
	IsDraft bool
}

func (article *Article) RawContent() template.HTML {
	return template.HTML(article.Content)
}

func (article *Article) ContentString() string {
	return string(article.Content)
}

func (article *Article) CateString() string {
	var str string
	for _, cate := range article.Cates {
		str += cate + "|"
	}
	if str != "" {
		return str[:len(str)-1]
	}
	return ""
}

func (article *Article) Date() string {
	return article.Time.In(loc).Format("2006-01-02 15:04 MST")
}

var tmplIndex *template.Template
var tmplArticle *template.Template
var tmplEditArticle *template.Template
var loc *time.Location

func init() {
	var err error
	loc, err = time.LoadLocation("Asia/Shanghai")
	if err != nil {
		panic(err)
	}
	tmplIndex, err = template.New("index").Parse(base_html + common_html + articlelist_html)
	if err != nil {
		panic(err)
	}
	fm := make(template.FuncMap)
	fm["gravatar"] = func(email string) string {
		h := md5.New()
		h.Write([]byte(email))
		return `http://www.gravatar.com/avatar/` + fmt.Sprintf("%x", h.Sum(nil)) + `?s=24&d=http%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D24`
	}
	tmplArticle, err = template.New("index").Funcs(fm).Parse(base_html + common_html + article_html)
	if err != nil {
		panic(err)
	}
	tmplEditArticle, err = template.New("index").Parse(base_html + common_html + articleedit_html)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/cate/", listArticleHandler)
	http.HandleFunc("/upload", uploadHandler)
	http.HandleFunc("/login", loginHandler)
	http.HandleFunc("/logout", logoutHandler)
	http.HandleFunc("/comment", commentHandler)
	http.HandleFunc("/rss", rssHandler)
	http.HandleFunc("/atom", atomHandler)
	http.Handle("/article/", http.StripPrefix("/article/", http.HandlerFunc(viewArticleHandler)))
	http.Handle("/file/", http.StripPrefix("/file/", http.HandlerFunc(fileHandler)))
	http.Handle("/new/", http.StripPrefix("/new/", http.HandlerFunc(editArticleHandler)))
	http.Handle("/edit/", http.StripPrefix("/edit/", http.HandlerFunc(editArticleHandler)))
	http.Handle("/delete/", http.StripPrefix("/delete/", http.HandlerFunc(deleteArticleHandler)))
}

const shortLen = 1000

func rssHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	q := datastore.NewQuery("article").Order("-Time").Limit(15)
	var articles []Article
	_, err := q.GetAll(c, &articles)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	rss := new(RSS)
	rss.Version = "2.0"
	rss.Channel.Title = "Blog@RqHuT"
	rss.Channel.Link = "http://blog.rqhut.net/"
	rss.Channel.Description = "hongrq'blog"
	rss.Channel.Language = "zh-cn"
	rss.Channel.Copyright = "Copyright 2012 Blog@RqHuT"
	rss.Channel.Generator = "Blog@RqHuT"
	rss.Channel.Item = make([]Item, len(articles))
	for i := range articles {
		rss.Channel.Item[i].Title = articles[i].Title
		rss.Channel.Item[i].Link = "http://blog.rqhut.net/article/" + articles[i].Title
		rss.Channel.Item[i].Description = xml.CharData(shortHTML(articles[i].Content, shortLen))
		rss.Channel.Item[i].Category = articles[i].Cates
		rss.Channel.Item[i].PubDate = articles[i].Time.Format(time.RFC1123Z)
		rss.Channel.Item[i].LastBuildDate = articles[i].Updated.Format(time.RFC1123Z)
	}
	if len(articles) != 0 {
		rss.Channel.LastBuildDate = time.Now().Format(time.RFC1123Z)
		rss.Channel.PubDate = articles[0].Time.Format(time.RFC1123Z)
	}
	w.Header().Set("Content-Type", "application/xml")
	w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>`))
	enc := xml.NewEncoder(w)
	err = enc.Encode(rss)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func atomHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	q := datastore.NewQuery("article").Order("-Time").Limit(15)
	var articles []Article
	_, err := q.GetAll(c, &articles)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	atom := NewAtom()
	atom.Title = "Blog@RqHuT"
	atom.SubTitle = "hongrq'blog"
	atom.Id = "http://blog.rqhut.net"
	atom.Generator = "Blog@RqHuT"
	atom.Entry = make([]Entry, len(articles))
	atom.Author.Name = "hongruiqi"
	atom.Author.Uri = "blog.rqhut.net"
	atom.Author.Email = "hongruiqi#gmail.com"
	for i := range articles {
		atom.Entry[i].Title = articles[i].Title
		atom.Entry[i].Id = "http://blog.rqhut.net/article/" + articles[i].Title
		atom.Entry[i].Link.Url = "http://blog.rqhut.net/article/" + articles[i].Title
		atom.Entry[i].Summary.Value = string(shortHTML(articles[i].Content, shortLen))
		atom.Entry[i].Summary.Type = "html"
		atom.Entry[i].Published = articles[i].Time.Format("2006-01-02T15:04:05Z")
		atom.Entry[i].Updated = articles[i].Updated.Format("2006-01-02T15:04:05Z")
	}
	if len(articles) != 0 {
		atom.Updated = time.Now().Format("2006-01-02T15:04:05Z")
		atom.Published = articles[0].Time.Format("2006-01-02T15:04:05Z")
	}
	w.Header().Set("Content-Type", "application/xml")
	w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>`))
	enc := xml.NewEncoder(w)
	err = enc.Encode(atom)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	url, err := user.LoginURL(c, "/")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, url, http.StatusMovedPermanently)
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	url, err := user.LogoutURL(c, "/")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, url, http.StatusMovedPermanently)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	var err error
	page := 1
	p := r.FormValue("page")
	if p != "" {
		page, err = strconv.Atoi(p)
		if err != nil || page < 1 {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}
	var articles []Article
	q := datastore.NewQuery("article").Order("-Time").Offset((page - 1) * 5).Limit(6)
	if !user.IsAdmin(c) {
		q = q.Filter("IsDraft =", false)
	}
	_, err = q.GetAll(c, &articles)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	for i := range articles {
		articles[i].Content = shortHTML(articles[i].Content, shortLen)
	}
	vars := make(map[string]interface{})
	vars["Page"] = page
	vars["Prev"] = page - 1
	if len(articles) == 6 {
		vars["Next"] = page + 1
		vars["Articles"] = articles[:len(articles)-1]
	} else {
		vars["Next"] = 0
		vars["Articles"] = articles
	}
	vars["IsAdmin"] = user.IsAdmin(c)
	err = tmplIndex.Execute(w, vars)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func listArticleHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	page := 1
	p := r.FormValue("page")
	cate := r.URL.Path[6:]
	var err error
	if p != "" {
		page, err = strconv.Atoi(p)
		if err != nil || page < 1 {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}
	var articles []Article
	q := datastore.NewQuery("article").Filter("Cates =", cate).Order("-Time").Offset((page - 1) * 5).Limit(6)
	if !user.IsAdmin(c) {
		q = q.Filter("IsDraft =", false)
	}
	_, err = q.GetAll(c, &articles)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	for i := range articles {
		articles[i].Content = shortHTML(articles[i].Content, shortLen)
	}
	vars := make(map[string]interface{})
	vars["Page"] = page
	vars["Prev"] = page - 1
	if len(articles) == 6 {
		vars["Next"] = page + 1
		vars["Articles"] = articles[:len(articles)-1]
	} else {
		vars["Next"] = 0
		vars["Articles"] = articles
	}
	vars["Cate"] = cate
	vars["IsAdmin"] = user.IsAdmin(c)
	err = tmplIndex.Execute(w, vars)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func viewArticleHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	title := r.URL.Path
	if title == "" {
		http.Error(w, "article not found", http.StatusNotFound)
		return
	}
	article := new(Article)
	k := datastore.NewKey(c, "article", title, 0, nil)
	err := datastore.Get(c, k, article)
	if err != nil {
		if err == datastore.ErrNoSuchEntity {
			http.Error(w, "article not found", http.StatusNotFound)
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	var comments []Comment
	q := datastore.NewQuery("comment").Filter("Article =", article.Title).Order("Time")
	_, err = q.GetAll(c, &comments)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	vars := make(map[string]interface{})
	vars["Article"] = article
	vars["IsAdmin"] = user.IsAdmin(c)
	vars["User"] = user.Current(c)
	vars["LoginURL"], err = user.LoginURL(c, "/article/"+article.Title)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	vars["comments"] = comments
	err = tmplArticle.Execute(w, vars)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func editArticleHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	if r.Method == "GET" {
		title := r.URL.Path
		if title == "" {
			tmplEditArticle.Execute(w, new(Article))
		} else {
			article := new(Article)
			k := datastore.NewKey(c, "article", title, 0, nil)
			err := datastore.Get(c, k, article)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			tmplEditArticle.Execute(w, article)
		}
	} else if r.Method == "POST" {
		article := new(Article)
		title := r.URL.Path
		article.Title = r.FormValue("title")
		if article.Title == "" {
			http.Error(w, "Title cannot be empty", http.StatusBadRequest)
			return
		}
		article.IsDraft = r.FormValue("IsDraft") == "on"
		article.Content = []byte(r.FormValue("content"))
		if article.Content == nil {
			http.Error(w, "Content cannot be empty", http.StatusBadRequest)
			return
		}
		article.Cates = strings.Split(r.FormValue("cates"), "|")
		err := datastore.RunInTransaction(c, func(c appengine.Context) error {
			var k *datastore.Key
			if title == "" {
				k = datastore.NewKey(c, "article", article.Title, 0, nil)
				err := datastore.Get(c, k, new(Article))
				if err != datastore.ErrNoSuchEntity {
					if err != nil {
						return err
					} else {
						return errors.New("title exists")
					}
				}
				article.Time = time.Now()
			} else {
				k = datastore.NewKey(c, "article", title, 0, nil)
				article_old := new(Article)
				err := datastore.Get(c, k, article_old)
				if err != datastore.ErrNoSuchEntity {
					if err != nil {
						return err
					}
				}
				article.Time = article_old.Time
				err = datastore.Delete(c, k)
				if err != nil {
					return err
				}
			}
			article.Updated = time.Now()
			k = datastore.NewKey(c, "article", article.Title, 0, nil)
			_, err := datastore.Put(c, k, article)
			if err != nil {
				return err
			}
			return nil
		}, &datastore.TransactionOptions{XG: true})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		http.Redirect(w, r, "/article/"+escape(article.Title, encodePath), http.StatusMovedPermanently)
	}
}

func deleteArticleHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	title := r.URL.Path
	k := datastore.NewKey(c, "article", title, 0, nil)
	err := datastore.Delete(c, k)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	} else {
		http.Redirect(w, r, "/", http.StatusMovedPermanently)
	}
}

type Comment struct {
	Article string
	Author  string
	Email   string
	Comment []byte
	Url     string
	Time    time.Time
}

func (comment *Comment) CommentString() string {
	return string(comment.Comment)
}

func (comment *Comment) Date() string {
	return comment.Time.In(loc).Format("2006-01-02 15:04 MST")
}

func commentHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	u := user.Current(c)
	if u == nil {
		http.Error(w, "Not login", http.StatusBadRequest)
		return
	}
	comment := new(Comment)
	comment.Article = r.FormValue("article")
	if comment.Article == "" {
		http.Error(w, "article not found", http.StatusBadRequest)
		return
	}
	comment.Author = r.FormValue("author")
	if comment.Author == "" {
		http.Error(w, "author cannot be empty", http.StatusBadRequest)
		return
	}
	comment.Comment = []byte(r.FormValue("comment"))
	if comment.Comment == nil {
		http.Error(w, "comment cannot be empty", http.StatusBadRequest)
		return
	}
	comment.Email = u.Email
	comment.Url = r.FormValue("url")
	comment.Time = time.Now()
	article := new(Article)
	k := datastore.NewKey(c, "article", comment.Article, 0, nil)
	err := datastore.Get(c, k, article)
	if err != nil {
		if err == datastore.ErrNoSuchEntity {
			http.Error(w, "article not found", http.StatusBadRequest)
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	k = datastore.NewIncompleteKey(c, "comment", nil)
	_, err = datastore.Put(c, k, comment)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/article/"+escape(comment.Article, encodePath), http.StatusMovedPermanently)
}

type File struct {
	Filename string
	Data     []byte
}

func fileHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	md5sum := r.URL.Path
	k := datastore.NewKey(c, "file", md5sum, 0, nil)
	file := new(File)
	err := datastore.Get(c, k, file)
	if err != nil {
		if err == datastore.ErrNoSuchEntity {
			http.Error(w, "", http.StatusNotFound)
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	w.Header().Set("Content-Type", http.DetectContentType(file.Data))
	w.Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
	w.Write(file.Data)
}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	filereader, filehead, err := r.FormFile("filedata")
	var fmtstr = `{"err":"%s","msg":{"url":"%s","localfile":"%s"}}`
	if err != nil {
		fmt.Fprintf(w, fmtstr, err.Error(), "", "")
		return
	}
	file := new(File)
	file.Filename = filehead.Filename
	file.Data, err = ioutil.ReadAll(filereader)
	if err != nil {
		fmt.Fprintf(w, fmtstr, err.Error(), "", "")
		return
	}
	h := md5.New()
	h.Write(file.Data)
	md5sum := fmt.Sprintf("%x", h.Sum(nil))
	k := datastore.NewKey(c, "file", md5sum, 0, nil)
	_, err = datastore.Put(c, k, file)
	if err != nil {
		fmt.Fprintf(w, fmtstr, err.Error(), "", "")
		return
	}
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, fmtstr, "", "/file/"+md5sum, file.Filename)
}
